#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Interactive dependency visualizer.

Usage example:

    python tools/dependency_visualizer.py \\
        --project /path/to/project \\
        --output output/dependencies.html \\
        --top 10 \\
        --radius 1

Generates:
  * An interactive HTML (PyVis) graph with hover tooltips, zoom/pan,
    node filtering controls via physics toggle.
  * (Optional) static PNG snapshot when --png is provided.
"""

from __future__ import annotations

import argparse
import os
import sys
from collections import defaultdict
from pathlib import Path
from typing import Iterable, List, Set

REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))

import networkx as nx
from pyvis.network import Network

from core.analysis_module.dependency_analyzer import DependencyAnalyzer
from core.builtin_config_module.builtin_config import BuiltInConfig
from core.utils_module.language_utils import detect_language_for_file, SUPPORTED_EXTENSIONS
from core.utils_module.logger import ObfuscationLogger, setup_logger


def build_file_groups(project_path: Path, exclude_dirs: Iterable[str]) -> defaultdict[str, List[str]]:
    groups: defaultdict[str, List[str]] = defaultdict(list)
    exclude = set(exclude_dirs)

    for root, dirs, files in os.walk(project_path):
        dirs[:] = [d for d in dirs if d not in exclude and not d.startswith(".")]
        for name in files:
            if Path(name).suffix.lower() not in SUPPORTED_EXTENSIONS:
                continue
            path = os.path.join(root, name)
            language = detect_language_for_file(path)
            if language:
                groups[language].append(path)

    return groups


def pick_core_nodes(graph: nx.DiGraph, top: int, radius: int, max_nodes: int | None) -> Set[str]:
    if not graph:
        return set()

    degrees = sorted(graph.out_degree(), key=lambda kv: kv[1], reverse=True)
    core_nodes = set()
    for node, _ in degrees[:top]:
        subgraph_nodes = nx.ego_graph(graph, node, radius=radius, undirected=False).nodes
        core_nodes.update(subgraph_nodes)
        if max_nodes and len(core_nodes) >= max_nodes:
            break

    if max_nodes:
        trimmed = set()
        for node in degrees:
            if node[0] in core_nodes:
                trimmed.add(node[0])
            if len(trimmed) >= max_nodes:
                break
        return trimmed

    return core_nodes


def substrate_graph(graph: nx.DiGraph, nodes: Set[str]) -> nx.DiGraph:
    return graph.subgraph(nodes).copy() if nodes else graph.copy()


def build_pyvis(
    graph: nx.DiGraph,
    output_html: Path,
    highlight_nodes: Set[str],
    central_nodes: List[str],
):
    net = Network(
        height=build_pyvis.canvas_height,
        width=build_pyvis.canvas_width,
        directed=True,
        notebook=False,
    )
    net.barnes_hut(gravity=-2000, central_gravity=0.3, spring_length=120)
    if build_pyvis.show_buttons:
        net.show_buttons(filter_=["physics"])

    for node in graph.nodes():
        path = Path(node)
        label = path.name
        tooltip = f"{label}<br>{node}"
        out_degree = graph.out_degree(node)
        in_degree = graph.in_degree(node)
        tooltip += f"<br>Out-degree: {out_degree} | In-degree: {in_degree}"

        color = "#1f77b4"
        size = 18
        if node in highlight_nodes:
            color = "#2ca02c"
            size = 22
        if node in central_nodes:
            color = "#ff7f0e"
            size = 28

        net.add_node(node, label=label, title=tooltip, color=color, size=size)

    for source, target in graph.edges():
        net.add_edge(source, target)

    output_html.parent.mkdir(parents=True, exist_ok=True)
    net.show(str(output_html), notebook=False)


def export_static_png(graph: nx.DiGraph, output_path: Path):
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt  # noqa: WPS433

    plt.figure(figsize=(20, 20), facecolor="white")
    pos = nx.spring_layout(graph, k=0.5, iterations=100, seed=42)
    labels = {node: Path(node).name for node in graph.nodes()}
    nx.draw_networkx_nodes(graph, pos, node_size=600, node_color="#1f77b4", alpha=0.75, edgecolors="white")
    nx.draw_networkx_edges(graph, pos, arrows=True, arrowstyle="-|>", arrowsize=12, width=1.0, edge_color="#cccccc")
    nx.draw_networkx_labels(graph, pos, labels, font_size=8, font_color="white")
    plt.axis("off")
    plt.tight_layout()
    output_path.parent.mkdir(parents=True, exist_ok=True)
    plt.savefig(output_path, dpi=180, facecolor="white")
    plt.close()


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Generate interactive dependency visualization.")
    parser.add_argument("--project", required=True, help="Path to the project root.")
    parser.add_argument("--output", default="output/dependencies.html", help="Output HTML path.")
    parser.add_argument("--top", type=int, default=10, help="Top-N nodes by out-degree to seed the subgraph.")
    parser.add_argument("--radius", type=int, default=1, help="Ego graph radius for highlighted seeds.")
    parser.add_argument("--max-nodes", type=int, default=200, help="Maximum nodes to include in visualization.")
    parser.add_argument("--png", help="Optional static PNG output path for snapshot.")
    parser.add_argument("--analyze-all", action="store_true", help="Skip filtering and render the entire graph.")
    parser.add_argument("--width", default="1600px", help="Canvas width for HTML visualization (e.g. 1600px or 100%).")
    parser.add_argument("--height", default="900px", help="Canvas height for HTML visualization.")
    parser.add_argument("--show-buttons", action="store_true", help="Expose PyVis control panel for physics/filters.")
    return parser.parse_args()


def main():
    args = parse_args()
    project_path = Path(args.project).resolve()
    if not project_path.exists():
        raise SystemExit(f"Project path not found: {project_path}")

    config = BuiltInConfig(project_path=str(project_path))
    logger = ObfuscationLogger(setup_logger("dependency_visualizer", level="INFO"))

    file_groups = build_file_groups(project_path, config.file_filters.exclude_directories)
    if not file_groups:
        raise SystemExit("No supported source files discovered.")

    analyzer = DependencyAnalyzer(config, logger)
    analyzer.analyze_dependencies(file_groups, symbols={})
    graph = analyzer.dependency_graph

    if args.analyze_all:
        working_graph = graph.copy()
        highlight_nodes: Set[str] = set()
        central_nodes = []
    else:
        picked_nodes = pick_core_nodes(graph, args.top, args.radius, args.max_nodes)
        working_graph = substrate_graph(graph, picked_nodes)
        highlight_nodes = picked_nodes
        central_nodes = [node for node, _ in sorted(graph.out_degree(), key=lambda kv: kv[1], reverse=True)[: args.top]]

    output_html = Path(args.output)
    build_pyvis.canvas_width = args.width
    build_pyvis.canvas_height = args.height
    build_pyvis.show_buttons = args.show_buttons

    build_pyvis(working_graph, output_html, highlight_nodes, central_nodes)

    if args.png:
        export_static_png(working_graph, Path(args.png))

    stats = analyzer.get_dependency_stats()
    print("Visualization complete.")
    print(f"Nodes in graph: {working_graph.number_of_nodes()} / {graph.number_of_nodes()}")
    print(f"Edges in graph: {working_graph.number_of_edges()} / {graph.number_of_edges()}")
    print("Dependency stats:", stats)


if __name__ == "__main__":
    main()
